package game;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.io.Serializable;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import communication.game.CellsPlayed;
import communication.game.ConfirmGameFinished;
import communication.game.NextPlayer;
import server.ServerInternals;

/**
 * Jeu complet du cote du serveur
 * @author Julien Roch
 * @version 1
 */
public class ServersideGame extends Game implements Serializable{

	private static final long serialVersionUID = 2320457045278045188L;
	// Grille avec les objets
	private GameObject 	grid[][];
	private String 		playedBy[][]; 
	private String		nextPlayer = null; // joueur qui a le droit de jouer
	private Map<String, Integer> scores;
	private Map<String, Color> colors;
	private ServerInternals internals;
	private Timer timer;
	private Socket playersSockets[];
	private long start = -1;
	
	private int nbMinesFound = 0;
	
	/**
	 * Instancie une partie
	 * @param name le nom
	 * @param creator nom du createur
	 * @param dimension taille de la grille
	 * @param nbPlayersMax nombre de joueurs maximum
	 * @param timePerPlayer Temps de jeu par joueur (sec)
	 * @param id Identifiant de la partie
	 * @param internals Model du serveur
	 * @param nbMines Nombre de mines
	 * @param nbObjMag Nombre d objets magiques
	 */
	public ServersideGame(String name, String creator, Dimension dimension, int nbPlayersMax, 
			int timePerPlayer, int id, ServerInternals internals, int nbMines, int nbObjMag) {
		super(name, creator, dimension, nbPlayersMax, timePerPlayer, id, nbMines, nbObjMag);
		this.internals = internals;
		this.scores = new HashMap<String, Integer>();
		this.colors = new HashMap<String, Color>();
		this.addPlayer(creator);
	}
	/**
	 * Initalise une partie
	 * @param objects
	 */
	public void intitialize(GameObject objects[]){
		List<GameObject> objectsTmp = Arrays.asList(objects);
		Collections.shuffle(objectsTmp);
		objects = objectsTmp.toArray(objects);
		
		// creation de nouvelles listes
		this.grid = new GameObject[this.dimension.width][this.dimension.height];
		this.playedBy = new String[this.dimension.width][this.dimension.height];
		
		// generation d'une liste de points temporaire (moche, mais ca marche...)
		ArrayList<Point> points = new ArrayList<Point>();
		for(int i=0;i<this.dimension.width;i++){
			for(int j=0;j<this.dimension.height;j++){
				points.add(new Point(i,j));
			}
		}
		Collections.shuffle(points);
		// on attribue les objets magiques...
		
		for(int i=0;i<objects.length;i++){
			Point p = points.get(i);
			grid[p.x][p.y] = objects[i];
			grid[p.x][p.y].setPoint(p);
		}
	}
	/**
	 * Indique qui est le prochain joueur
	 */
	public void setNextPlayer(){
		this.setNextPlayer(this.players.get((this.players.indexOf(this.nextPlayer)+1)%this.players.size()));
	}
	/**
	 * Definit le prochain joueur
	 * @param player Le prochain joueur
	 */
	public void setNextPlayer(String player){
		this.nextPlayer = player;
		internals.sendDataToClients(playersSockets, 
				new NextPlayer(new MinimalGame(this), new Player(player,this)));
	}
	/**
	 * Classe pour changer de joueur
	 * @author Julien Roch
	 * @version 1.0
	 */
	private class ChangePlayer extends TimerTask{
		public void run() {
			ServersideGame.this.setNextPlayer();
		}
	}
	
	/**
	 * Rajoutte un joueur a une partie
	 * @param player le joueur
	 */
	public void addPlayer(String player){
		super.addPlayer(player);
		scores.put(player, 0);
		colors.put(player, new Color((float)Math.random(),(float)Math.random(),(float)Math.random()));
		generateColorsForPlayers();

		this.generateSocketList();
	}
	/**
	 * Recalcul la couleur des joueurs
	 */
	public void generateColorsForPlayers(){
		int size = players.size();
		for(int i = 0 ; i < size; i++){
			float p = ((float) i) / size;
			colors.put(players.get(i), Color.getHSBColor(p, 1f, 1f));
		}
	}
	/**
	 * Obtient les sockets des joueurs de la partie
	 */
	private void generateSocketList(){
		// on recupere la liste des sockets
		playersSockets = new Socket[players.size()];
		int i = 0;
		for(String p : players){
			Socket s = null;
			for(Socket key : internals.getClients().keySet()){
				if(internals.getClients().get(key).equals(p)){
					s = key;
					break;
				}
			}
			playersSockets[i++] = s;
		}
	}
	/**
	 * Supprime un joueur d'une partie
	 * @param player Le nom du joueur a supprimer
	 */
	public void removePlayer(String player){
		super.removePlayer(player);
		this.generateSocketList();
	}
	
	/**
	 * Joue une case
	 * @param point la case a jouer
	 * @param player le joueur
	 * @param object l'objet magique
	 */
	public void playCell(Point point, String player, MagicObject object){
		// Initialise le timer
		if(this.start<0){
			Date d = new Date();
			this.start = d.getTime();
		}
		
		if(player.equals(this.nextPlayer) && playedBy[point.x][point.y]==null){	// la case n'a pas ete jouee et c'est le tour du bon joueur
			if(this.timer!=null)
				this.timer.cancel();
			if(!Mine.class.isInstance(grid[point.x][point.y])){
				// on force le prochain joueur
				this.setNextPlayer();
			}
			
			ArrayList<Point> casesAJouer = new ArrayList<Point>(); // cases devant etre examinees
			casesAJouer.add(point);
			if(object!=null){
				object.setPoint(point);
				Point[] pts = object.makeSpecialAction(this, player);
				for(Point p : pts)
					casesAJouer.add(p);
			}
			ArrayList<GameObject> objetsAEnvoyer = new ArrayList<GameObject>();
			while(casesAJouer.size()!=0){	// il reste des cases a jouer
				Point p = casesAJouer.get(0);
				casesAJouer.remove(0);
				if(playedBy[p.x][p.y]!=null)
					continue;
				setPlayedBy(p, player); // on joue la case
				if(nbMines==nbMinesFound){
					timer.cancel();
					// le jeu est fini...
					String players[] = this.getPlayers();
					// enregistrement des scores
					for(String pla: players){
						internals.addPoints(pla, scores.get(pla));
					}
					String scores[] = new String[players.length];
					for(int i=0;i<players.length;i++){
						scores[i] = this.scores.get(players[i]).toString();
					}
					internals.sendDataToClients(this.playersSockets, new ConfirmGameFinished(new MinimalGame(this), (int)(new Date().getTime()-start)/1000, this.getPlayersAsPlayer()));
					return;
				}
				// on ajoute l'objet a envoyer a la liste
				if(grid[p.x][p.y]==null){				
					objetsAEnvoyer.add(	new EmptyCell(p, nbMinesArount(p)));
				}else{
					if(EmptyCell.class.isInstance(grid[p.x][p.y]))
						((EmptyCell)grid[p.x][p.y]).setNbMinesAround( nbMinesArount(p));
					objetsAEnvoyer.add(grid[p.x][p.y]);
					if(object ==  null && Mine.class.isInstance(grid[p.x][p.y]))
						break; // on ne decouvre pas les voisins
				}
				// on rajoute ses eventuels voisins, tant qu'elle n'est pas soit une mine soit une case a coté d'une mine
				// et qu'on ne joue pas d objet magique
				if(nbMinesArount(p)==0 && object==null){
					for(int i=Math.max(0, p.x-1);i<=Math.min(dimension.width-1, p.x+1);i++){
						for(int j=Math.max(0, p.y-1);j<=Math.min(dimension.height-1, p.y+1);j++){
							Point nextToAdd = new Point(i,j);
							if(hasToAddCell(nextToAdd)){
								casesAJouer.add(nextToAdd);
							}
						}
					}
				}
				// on a fait le tour des voisins
			}
			// on a fait le tour des cases de la zone, ont peut donc envoyer la sauce
			
			internals.sendDataToClients(playersSockets, new CellsPlayed(new MinimalGame(this), objetsAEnvoyer.toArray(new GameObject[objetsAEnvoyer.size()]),point, this.getPlayersAsPlayer(), new Player(player, this)));
			timer = new Timer();
			timer.schedule(new ChangePlayer(), 60000*timePerPlayer, 60000*timePerPlayer);
		}
	}
	/**
	 * Indique le nombre de mines autour d'une case
	 * @param p La case
	 * @return Le nombre de mine aux allentours
	 */
	private int nbMinesArount(Point p){
		int n=0;
		for(int i=Math.max(0, p.x-1);i<=Math.min(dimension.width-1, p.x+1);i++){
			for(int j=Math.max(0, p.y-1);j<=Math.min(dimension.height-1, p.y+1);j++){
				if(Mine.class.isInstance(grid[i][j])){
					n++;
				}
			}
		}
		return n;
	}
	/**
	 * Indique si une case sans mine n'a pas ete jouee
	 * @todo commentaire parlant
	 * @param p La case
	 * @return case non jouee ou pas
	 */
	private boolean hasToAddCell(Point p){
		return playedBy[p.x][p.y] == null										// case pas encore jouee
		&& !Mine.class.isInstance(grid[p.x][p.y])											// case non minee
		;
	}
	
	/**
	 * Defini par qui une case a ete jouee et applique le visiteur
	 * @param point la case
	 * @param player le joueur qui a joue la case
	 */
	void setPlayedBy(Point point, String player){
		this.playedBy[point.x][point.y] = player;
		
		if(grid[point.x][point.y]!=null){ // objet special, genre mine... on applique le visiteur
			grid[point.x][point.y].visit(this, player);
		}
	}

	/**
	 * Renvoie les sockets des joueurs
	 * @return les sockets des joueurs
	 */
	public Socket[] getPlayersSockets() {
		return playersSockets;
	}
	/**
	 * Initialise les joueurs
	 * et definit qui commence
	 */
	public void initializePlayers(){
		// on melange l'ordre des joueurs
		Collections.shuffle(players);
		setNextPlayer(players.get(0));
	}
	/**
	 * Ajoute des points a un joueur
	 * @param player Le nom du joueur
	 * @param pointsToAdd Le nombre de points a ajouter
	 */
	public void addPoints(String player, int pointsToAdd){
		int points = 0;
		if(scores.get(player)!=null){
			points = scores.get(player).intValue();
		}
		points += pointsToAdd;
		scores.put(player, points);
	}
	/**
	 * Obtient les scores des joueurs
	 * @return les scores des joueurs
	 */
	public Map<String, Integer> getScores(){
		return this.scores;
	}
	/**
	 * Obtient le temps de partie
	 * @return le temps de partie
	 */
	public Timer getTimer(){
		return this.timer;
	}

	/**
	 * Obitient le nombre de mines trouvees
	 * @return  le nombre de mines trouvees
	 */
	public int getNbMinesFound() {
		return nbMinesFound;
	}
	/**
	 * Obtitient le temps du debut de partie
	 * @return le temps de debut de partie
	 */
	public long getStart(){
		return this.start;
	}
	/**
	 * Obtient la listes des joueurs de la partie
	 * @return La liste des joueurs
	 */
	public Player[] getPlayersAsPlayer(){
		Player p[] = new Player[players.size()];
		for(int i=0;i<players.size();i++){
			p[i] = new Player(players.get(i), this);
		}
		return p;
	}
	/**
	 * Obitent la couleur d'un joueur
	 * @param player
	 * @return la couleur
	 */
	public Color getColorForPlayer(String player){
		return colors.get(player);
	}
	/**
	 * Definit le nombre de mines trouvees
	 * @param nbMinesFound le nombre de mines trouvees
	 */
	public void setNbMinesFound(int nbMinesFound) {
		this.nbMinesFound = nbMinesFound;
	}
}